/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: HTTPConnection.java,v $
 *
 */

package com.cidero.http;

import java.io.InputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.InetAddress;
import java.util.logging.Logger;

import com.cidero.util.BufferedReaderInputStream;

/**
 *  HTTP connection class.  The connection class is used to provide
 *  persistent HTTP connections (multiple GET/POST/etc... requests
 *  on the same socket)
 *
 *  Client example to download 'file.txt':
 *
 *     HTTPConnection connection = new HTTPConnection();
 *
 *     // Set up connection-specifics...
 *     // conn.setPersistentConnection( true );
 * 
 *     try
 *     {
 *       HTTPRequest req = new HTTPRequest( HTTP.GET, url );
 *       getReq.addHeader("Accept: audio/mpeg");
 *     }
 *  
 *     try
 *     {
 *       response = connection.sendRequest( req );
 *     }
 *     catch( HttpException e )
 *     {
 *       System.out.println("Error executing HTTP request" + e );
 *     }
 *
 *     try
 *     {
 *       if( response.getStatusCode() == HTTPStatus.OK )
 *       {
 *         byte[] content = response.getResponseContent(); 
 *         // or String content = response.getResponseContentString(); 
 *
 *         // Processing...
 *
 *         response.releaseConnection();
 *       }
 *     }
 *     catch( HttpException e )
 *     {
 *       System.out.println("Error executing HTTP request" + e );
 *     }
 *
 *     System.out.println("Response:\n" + response.toString );
 *
 *
 *  Server side (single HTTP session with optional persistence)
 *
 *     AcceptorThread:
 *
 *       socket = serverSocket.accept()
 *       serverThread = new HTTPServerThread( socket );
 *
 *     ServerThread:
 *
 *       HTTPConnection connection = new HTTPConnection( socket );
 *
 *       // Use the same session to process multiple requests if
 *       // 1st request indicates that client supports persistent
 *       // connections
 * 
 *       while( ! sessionDone )
 *       {
 *          HTTPRequest req = connection.receiveRequest();
 *
 *          // Process request...
 *
 *          // Create response
 *          HTTPResponse response = new HTTPResponse();
 *
 *          connection.sendResponse( response );
 *
 *          //
 *          // or, to support streaming server
 *          //
 *          // connection.sendResponseHeader( response );
 *          // outStream = response.getOutputStream();
 *          // while ( !done )
 *          //   outStream.send( buf, bytes );
 *
 *       }
 *
 */
public class HTTPConnection
{
  private static Logger logger = Logger.getLogger("com.cidero.http");

  private String     requestHost = null;
  private int        requestPort = -1;
  private boolean    persistent = false;

  private Socket                     sock;
  private InputStream                sockRawInStream;
  private BufferedReaderInputStream  sockInStream;
  private BufferedOutputStream       sockOutStream;

  // Last request on connection
  HTTPRequest httpReq;

	/**
   *	Constructor
   */
  public HTTPConnection()
  {
    
	}

  /**
   * Constructor for server side. Uses the socket returned by the 
   * server accept() call.
   */
  public HTTPConnection( Socket serverSocket ) throws IOException
  {
    sock = serverSocket;

    sockRawInStream = sock.getInputStream();
    sockInStream = new BufferedReaderInputStream( sock.getInputStream() );
    sockOutStream = new BufferedOutputStream( sock.getOutputStream() );
  }

  public void connect() throws IOException
  {
    //
    // If host is in IP '.' notation, create a InetAddr here and use
    // that to open the socket. This allows the Socket constructor to
    // bypass some name resolution logic than can introduce significant
    // latency under Windows when TCP/IP NETBIOS name resolution is in
    // effect.
    //
    // Split pattern of "\D" means split when not a digit ('.'). Helps
    // screen out non-numeric hosts with '.''s  (e.g. java.sun.com.net) 

    String[] hostByteStrings = requestHost.split("\\D");
    if( hostByteStrings.length == 4 )
    {
      byte[] hostBytes = new byte[4];
      for( int n = 0 ; n < 4 ; n++ )
        hostBytes[n] = (byte)Integer.parseInt( hostByteStrings[n] );

      //System.out.println("-!!--post: Creating Inet4Address for " + host );
      InetAddress inetAddr = InetAddress.getByAddress( requestHost,
                                                       hostBytes );
      //System.out.println("-!!--creating socket " );
      sock = new Socket( inetAddr, requestPort );
    }
    else
    {
      sock = new Socket( requestHost, requestPort );
    }
    
    sockRawInStream = sock.getInputStream();
    sockInStream = new BufferedReaderInputStream( sock.getInputStream() );
    sockOutStream = new BufferedOutputStream( sock.getOutputStream() );
    logger.fine("Connected to server socket - host: " + requestHost +
                " port: " + requestPort );
  }

  public BufferedReaderInputStream getInputStream()
  {
    return sockInStream;
  }

  public InputStream getRawInputStream()
  {
    return sockRawInStream;
  }

  public BufferedOutputStream getOutputStream()
  {
    return sockOutStream;
  }
  
  /**
   *  Send HTTP request and return response. Only the header part 
   *  of the response is actually read. 
   *
   *  @param  request
   *  @param  getResponseContentFlag
   *  @return HTTP response
   */
  public HTTPResponse sendRequest( HTTPRequest request,
                                   boolean getResponseContentFlag )
    throws IOException
  {
    logger.fine("sendRequest: Entered - host: " + requestHost +
                " port: " + requestPort );

    //
    // If no socket is currently open for the requested host/port,
    // open one.   is the same as the current connection (persistent)
    // just reuse it - otherwise open new socket 
    //
    if ( !persistent || (sock == null) ||
         ! request.getHost().equals(requestHost) ||
         (request.getPort() != requestPort) )
    {
      try
      {
        if (sock != null)
          close();
        requestHost = request.getHost();
        requestPort = request.getPort();
        connect();
      }
      catch (IOException e)
      {
        sock = null;
        throw new IOException("Error connecting to HTTP server");
      }
    }
    else
    {
      logger.fine("Reusing existing connection - host: " + requestHost +
                  " port: " + requestPort );
    }
    
    // 
    // Send request, get header portion of response. 
    //
    HTTPResponse response;
    try
    {
      // HTTPPacket write routine used for both requests & responses
      logger.fine("sending request");
      request.write( sockOutStream );  

      logger.fine("waiting for response");
      response = new HTTPResponse( this, getResponseContentFlag );
      logger.fine("got response");
    }
    catch (IOException e)
    {
      logger.fine("Exception sending request");
      e.printStackTrace();
      
      close();
      response = new HTTPResponse();
      response.setStatusCode( HTTPStatus.INTERNAL_SERVER_ERROR );
    }

    // If not a persistent connection, and response content was
    // already read, we are done with this connection - close it
    // (Next use of connection will use fresh socket)
    if( !persistent && (getResponseContentFlag == true) )
      close();
    
    return response;
  }

  /**
   * Release connection. Invoked after application has finished reading
   * responses input stream.  Normally invoked indirectly via the
   * HTTPResponse.releaseConnection() method.
   */
  public void release()
  {
    // If persistent connection, leave socket open so it can be reused
    if( persistent )
      return;
      
    close();
  }

  /**
   * Receive a request from an HTTP client
   */ 
  public HTTPRequest receiveRequest()
    throws IOException
  {
    // save request reference for later access connection object (convenience)
    httpReq = new HTTPRequest( this );
    return httpReq;
  }
  public HTTPRequest receiveRequest( boolean getContent )
    throws IOException
  {
    // save request reference for later access connection object (convenience)
    httpReq = new HTTPRequest( this, getContent );
    return httpReq;
  }

  /**
   * Send a response to an HTTP client
   */ 
  public void sendResponse( HTTPResponse response )
    throws IOException
  {
    response.write( sockOutStream );
  }

  /**
   * Send the header portion of a response to an HTTP client (to support
   * streaming server)
   */ 
  public void sendResponseHeader( HTTPResponse response )
    throws IOException
  {
    response.writeHeader( sockOutStream );
  }

  
  public void close()
  {
    logger.fine("Closing connection");

    try
    {
      /*
      if( sockOutStream != null )
      {
        System.out.println("Closing sock output stream");
        sockOutStream.close();
      }

      if( sockInStream != null )
      {
        System.out.println("Closing sock input stream");
        sockInStream.close();
      }
      */
      
      if( sock != null )
      {
        //System.out.println("Calling sock.close()");
        //sock.shutdownInput();
        //sock.shutdownOutput();
        sock.close();
      }

      //System.out.println("Leaving HTTPConnection.close()");

    }
    catch (Exception e)
    {
      logger.warning("Exception closing HTTP Connection" + e );
    }
    
    sockInStream = null;
    sockOutStream = null;
    sock = null;
  }
  

  public void finalize()
  {
    close();
  }

	public Socket getSocket()
  {
		return sock;
	}

	/**
   *	Get local address of connection socket
   */
	public String getLocalAddress()
	{
		return sock.getLocalAddress().getHostName();	
	}

	/**
   *	Get remote address of connection socket
   */
	public String getRemoteAddress()
	{
		return sock.getInetAddress().getHostAddress();	
	}

  public HTTPRequest getRequest()
  {
    return httpReq;
  }

	/**
   *	Get local port of HTTP request socket
   */
	public int getLocalPort()
	{
		return sock.getLocalPort();	
	}

	public void setRequestHost(String host) {
    requestHost = host;
  }
	public String getRequestHost() {
    return requestHost;
  }
  
	public void setRequestPort(int host) {
		requestPort = host;
	}
	public int getRequestPort() {
		return requestPort;
	}

	public int getReceiveBufferSize()
	{
    try 
    {
      return sock.getReceiveBufferSize();
    }
    catch( SocketException e )
    {
      logger.warning("Failed to get socket send buffer size " + e );
      return -1;
    }
  }
	public void setReceiveBufferSize( int size )
	{
    try 
    {
      sock.setReceiveBufferSize( size );
    }
    catch( SocketException e )
    {
      logger.warning("Failed to get socket send buffer size " + e );
    }
  }

	public int getSendBufferSize()
	{
    try 
    {
      return sock.getSendBufferSize();
    }
    catch( SocketException e )
    {
      logger.warning("Failed to get socket send buffer size " + e );
      return -1;
    }
  }
	public void setSendBufferSize( int size )
	{
    try 
    {
      sock.setSendBufferSize( size );
    }
    catch( SocketException e )
    {
      logger.warning("Failed to set socket send buffer size " + e );
    }
  }

	public void setTcpNoDelay( boolean on )
	{
    try 
    {
      sock.setTcpNoDelay( on );
    }
    catch( SocketException e )
    {
      logger.warning("Failed to set socket TCP_NDELAY mode " + e );
    }
  }
  

}
